package org.octopus.data.desensitized.core.processor.impl;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.octopus.data.desensitized.core.annotation.Desensitized;
import org.octopus.data.desensitized.core.annotation.DesensitizedRule;
import org.octopus.data.desensitized.core.processor.DesensitizedProcessor;
import org.octopus.data.desensitized.core.registry.GlobalDesensitizedStrategyRegistry;
import org.octopus.data.desensitized.core.strategy.DesensitizedStrategy;
import org.octopus.data.desensitized.core.util.DesensitizedUtils;

/**
 * @author lemon
 * @date 2021/8/27 12:40
 */
public class DefaultDesensitizedProcessor implements DesensitizedProcessor {

    @Override
    public final <T> T process(T original) {
        if (original == null) {
            return null;
        }

        Class<?> clazz = original.getClass();

        if (clazz.isPrimitive() || clazz.isAnnotation() || clazz.isEnum()) {
            return original;
        }

        return (T) this.doProcessObject(original);
    }

    private Object doProcessObject(Object original) {
        if (original == null) {
            return null;
        }

        if (original instanceof Collection) {
            Collection collection = (Collection) original;

            if (CollectionUtils.isNotEmpty(collection)) {
                collection.forEach(element -> this.doProcessObject(element));
            }

            return original;
        } else if (original instanceof Map) {
            Map map = (Map) original;

            if (MapUtils.isNotEmpty(map)) {
                map.values().forEach(element -> this.doProcessObject(element));
            }

            return original;
        } else if (original.getClass().isArray()) {
            this.doProcessArray(original);
            return original;
        }

        Field[] fields = original.getClass().getDeclaredFields();
        AccessibleObject.setAccessible(fields, true);
        Arrays.stream(fields).forEach(field -> {
            try {
                Object fieldValue = field.get(original);
                this.doProcessFieldValue(original, fieldValue, field);
            } catch (IllegalAccessException e) {
                throw new InternalError("Unexpected IllegalAccessException: " + e.getMessage());
            }
        });

        return original;
    }

    private void doProcessArray(Object original) {
        for (int i = 0; i < Array.getLength(original); ++i) {
            Object element = Array.get(original, i);
            this.doProcessObject(element);
        }
    }

    private void doProcessFieldValue(Object originalObject, Object fieldValue, Field field) {
        if (fieldValue == null || (field.getType().isPrimitive() && !field.getType().equals(String.class))) {
            return;
        }

        if (field.getType().equals(String.class)) {
            this.doUpdateFieldValueForStringType(originalObject, (String) fieldValue, field);
        } else {
            this.doProcessObject(fieldValue);
        }
    }

    private void doUpdateFieldValueForStringType(Object originalObject, String fieldValue, Field field) {
        Desensitized desensitized = field.getAnnotation(Desensitized.class);

        if (desensitized == null || !desensitized.enable()) {
            return;
        }

        String newValue = this.doDesensitizedFiledValueForStringType(fieldValue, desensitized);

        if (newValue != null && newValue != fieldValue) {
            try {
                field.set(originalObject, newValue);
            } catch (IllegalAccessException e) {
                throw new InternalError("Unexpected IllegalAccessException: " + e.getMessage());
            }
        }
    }

    private String doDesensitizedFiledValueForStringType(String primitiveObject, Desensitized desensitized) {
        if (desensitized == null || !desensitized.enable()) {
            return primitiveObject;
        }

        DesensitizedRule desensitizedRule = DesensitizedUtils.buildDesensitizedRule(desensitized);

        DesensitizedStrategy<String> desensitizedStrategy =
                GlobalDesensitizedStrategyRegistry.obtainDesensitizedStrategy(String.class,
                        desensitized.desensitizedPolicy());

        if (desensitizedStrategy != null) {
            return desensitizedStrategy.convert(primitiveObject, desensitizedRule);
        }

        return primitiveObject;
    }
}
